
package com.mancj.slideup;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.Component;
import ohos.multimodalinput.event.TouchEvent;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;

import static com.mancj.slideup.SlideUp.State.HIDDEN;
import static com.mancj.slideup.SlideUp.State.SHOWED;

/**
 * Slide up
 */
public class SlideUp implements Component.TouchEventListener, ValueAnimator.ValueUpdateListener, Animator.StateChangedListener, LoggerNotifier {
    /**
     * * The constant TAG
     */
    private final static String TAG = SlideUp.class.getSimpleName();

    /**
     * * The constant KEY_START_GRAVITY
     */
    final static String KEY_START_GRAVITY = TAG + "_start_gravity";
    /**
     * * The constant KEY_DEBUG
     */
    final static String KEY_DEBUG = TAG + "_debug";
    /**
     * * The constant KEY_TOUCHABLE_AREA
     */
    final static String KEY_TOUCHABLE_AREA = TAG + "_touchable_area";
    /**
     * * The constant KEY_STATE
     */
    final static String KEY_STATE = TAG + "_state";
    /**
     * * The constant KEY_AUTO_SLIDE_DURATION
     */
    final static String KEY_AUTO_SLIDE_DURATION = TAG + "_auto_slide_duration";
    /**
     * * The constant KEY_HIDE_SOFT_INPUT
     */
    final static String KEY_HIDE_SOFT_INPUT = TAG + "_hide_soft_input";
    /**
     * * The constant KEY_STATE_SAVED
     */
    final static String KEY_STATE_SAVED = TAG + "_state_saved";
    /**
     * The constant M current state
     */
    private State mCurrentState;
    /**
     * The constant M view height
     */
    private float mViewHeight;
    /**
     * The constant M view width
     */
    private float mViewWidth;
    /**
     * The constant M builder
     */
    private SlideUpBuilder mBuilder;
    /**
     * The constant M vertical touch consumer
     */
    private VerticalTouchConsumer mVerticalTouchConsumer;
    /**
     * The constant M horizontal touch consumer
     */
    private HorizontalTouchConsumer mHorizontalTouchConsumer;
    /**
     * The constant M animation processor
     */
    private AnimationProcessor mAnimationProcessor;

    /**
     * Slide up
     *
     * @param builder builder
     */
    SlideUp(SlideUpBuilder builder) {
        mBuilder = builder;
        init();
    }

    /**
     * Init Data
     */
    private void init() {
        mBuilder.mSliderView.setTouchEventListener(this);
        if (mBuilder.mAlsoScrollView != null) {
            mBuilder.mAlsoScrollView.setTouchEventListener(this);
        }

        mBuilder.mSliderView.setLayoutRefreshedListener(new Component.LayoutRefreshedListener() {
            @Override
            public void onRefreshed(Component component) {
                mViewHeight = mBuilder.mSliderView.getHeight();
                mViewWidth = mBuilder.mSliderView.getWidth();
                switch (mBuilder.mStartGravity) {
                    case Gravity.TOP:
                        mBuilder.mSliderView.setPivotY(mViewHeight);
                        setTouchableAreaVertical();
                        break;
                    case Gravity.BOTTOM:
                        mBuilder.mSliderView.setPivotY(0);
                        setTouchableAreaVertical();
                        break;
                    case Gravity.START:
                        mBuilder.mSliderView.setPivotX(0);
                        setTouchableAreaHorizontal();
                        break;
                    case Gravity.END:
                        mBuilder.mSliderView.setPivotX(mViewWidth);
                        setTouchableAreaHorizontal();
                        break;
                }
                int[] locationOnScreen = mBuilder.mSliderView.getLocationOnScreen();
                setLocationOnScreen(locationOnScreen[0], locationOnScreen[1]);
            }
        });
        createConsumers();
        updateToCurrentState();
    }

    /**
     * Set touchable area horizontal
     */
    private void setTouchableAreaHorizontal() {
        if (mBuilder.mTouchableArea == 0) {
            mBuilder.mTouchableArea = (float) Math.ceil(mViewWidth / 10);
        }
    }

    private void setLocationOnScreen(int locationOnScreenX, int locationOnScreenY) {
        if (mBuilder.locationOnScreenX == 0) {
            mBuilder.locationOnScreenX = locationOnScreenX;
        }
        if (mBuilder.locationOnScreenY == 0) {
            mBuilder.locationOnScreenY = locationOnScreenY;
        }

    }

    /**
     * Set touchable area vertical
     */
    private void setTouchableAreaVertical() {
        if (mBuilder.mTouchableArea == 0) {
            mBuilder.mTouchableArea = (float) Math.ceil(mViewHeight / 10);
        }
    }

    /**
     * Create animation
     */
    private void createAnimation() {
        mAnimationProcessor = new AnimationProcessor(mBuilder, this, this);
    }

    /**
     * Create consumers
     */
    private void createConsumers() {
        createAnimation();
        mVerticalTouchConsumer = new VerticalTouchConsumer(mBuilder, this, mAnimationProcessor);
        mHorizontalTouchConsumer = new HorizontalTouchConsumer(mBuilder, this, mAnimationProcessor);
    }

    /**
     * Update to current state
     */
    private void updateToCurrentState() {
        switch (mBuilder.mStartState) {
            case HIDDEN:
                hideImmediately();
                break;
            case SHOWED:
                showImmediately();
                break;
        }
    }

    /**
     * <p>Trying hide soft input from window</p>
     *
     * @see, int )
     */
    public void hideSoftInput() {
    }


    /**
     * <p>Trying show soft input to window</p>
     * <p>
     * int)
     */
    public void showSoftInput() {
    }

    /**
     * <p>Returns the visibility status for this view.</p>
     *
     * @return true if view have status {@lin
     */
    public boolean isVisible() {
        return mBuilder.mSliderView.getVisibility() == Component.VISIBLE;
    }

    /**
     * Add Listener which will be used in combination with this SlideUp
     *
     * @param listener listener
     */
    public void addSlideListener(Listener listener) {
        mBuilder.mListeners.add(listener);
    }

    /**
     * <p>Remove Listener which was used in combination with this SlideUp</p>
     *
     * @param listener listener
     */
    public void removeSlideListener(Listener listener) {
        mBuilder.mListeners.remove(listener);
    }

    /**
     * <p>Returns typed view which was used as slider</p>
     *
     * @param <T> parameter
     * @return the t
     */
    public <T extends Component> T getSliderView() {
        return (T) mBuilder.mSliderView;
    }

    /**
     * <p>Returns duration of animation (whenever you use {@link #hide()} or {@link #show()} methods)</p>
     *
     * @return the float
     */
    public float getAutoSlideDuration() {
        return mBuilder.mAutoSlideDuration;
    }

    /**
     * <p>Set duration of animation (whenever you use {@link #hide()} or {@link #show()} methods)</p>
     *
     * @param autoSlideDuration <b>(default - <b color="#EF6C00">300</b>)</b>
     */
    public void setAutoSlideDuration(int autoSlideDuration) {
        mBuilder.withAutoSlideDuration(autoSlideDuration);
        mAnimationProcessor.paramsChanged();
    }

    /**
     * <p>Returns touchable area <b>(in dp)</b> for interaction</p>
     *
     * @return the float
     */
    public float getTouchableAreaDp() {
        return mBuilder.mTouchableArea / mBuilder.mDensity;
    }

    /**
     * <p>Set touchable area <b>(in dp)</b> for interaction</p>
     *
     * @param touchableArea <b>(default - <b color="#EF6C00">300dp</b>)</b>
     */
    public void setTouchableAreaDp(float touchableArea) {
        mBuilder.withTouchableAreaDp(touchableArea);
    }

    /**
     * <p>Returns touchable area <b>(in px)</b> for interaction</p>
     *
     * @return the float
     */
    public float getTouchableAreaPx() {
        return mBuilder.mTouchableArea;
    }

    /**
     * <p>Set touchable area <b>(in px)</b> for interaction</p>
     *
     * @param touchableArea <b>(default - <b color="#EF6C00">300dp</b>)</b>
     */
    public void setTouchableAreaPx(float touchableArea) {
        mBuilder.withTouchableAreaPx(touchableArea);
    }

    /**
     * <p>Returns running status of animation</p>
     *
     * @return true if animation is running
     */
    public boolean isAnimationRunning() {
        return mAnimationProcessor.isAnimationRunning();
    }

    /**
     * <p>Show view with animation</p>
     */
    public void show() {
        show(false);
    }

    /**
     * <p>Hide view with animation</p>
     */
    public void hide() {
        hide(false);
    }

    /**
     * <p>Hide view without animation</p>
     */
    public void hideImmediately() {
        hide(true);
    }

    /**
     * <p>Show view without animation</p>
     */
    public void showImmediately() {
        show(true);
    }

    /**
     * <p>Returns current status of debug logging</p>
     *
     * @return the boolean
     */
    public boolean isLoggingEnabled() {
        return mBuilder.mDebug;
    }

    /**
     * <p>Turning on/off debug logging</p>
     *
     * @param enabled <b>(default - <b color="#EF6C00">false</b>)</b>
     */
    public void setLoggingEnabled(boolean enabled) {
        mBuilder.withLoggingEnabled(enabled);
    }

    /**
     * <p>Returns current status of gestures</p>
     *
     * @return the boolean
     */
    public boolean isGesturesEnabled() {
        return mBuilder.mGesturesEnabled;
    }

    /**
     * <p>Turning on/off gestures</p>
     *
     * @param enabled <b>(default - <b color="#EF6C00">true</b>)</b>
     */
    public void setGesturesEnabled(boolean enabled) {
        mBuilder.withGesturesEnabled(enabled);
    }

    /**
     * <p>Returns current interpolator</p>
     *
     * @return the int
     */
    public int getInterpolator() {
        return mBuilder.mInterpolator;
    }

    /**
     * <p>Sets interpolator for animation (whenever you use {@link #hide()} or {@link #show()} methods)</p>
     *
     * @param interpolator <b>(default - <b color="#EF6C00">Decelerate interpolator</b>)</b>
     */
    public void setInterpolator(int interpolator) {
        mBuilder.withInterpolator(interpolator);
        mAnimationProcessor.paramsChanged();
    }

    /**
     * <p>Returns gravity which used in combination with this SlideUp</p>
     *
     * @return the int
     */
    @StartVector
    public int getStartGravity() {
        return mBuilder.mStartGravity;
    }

    /**
     * <p>Returns current behavior of soft input</p>
     *
     * @return the boolean
     */
    public boolean isHideKeyboardWhenDisplayed() {
        return mBuilder.mHideKeyboard;
    }

    /**
     * <p>Sets behavior of soft input</p>
     *
     * @param hide <b>(default - <b color="#EF6C00">false</b>)</b>
     */
    public void setHideKeyboardWhenDisplayed(boolean hide) {
        mBuilder.withHideSoftInputWhenDisplayed(hide);
    }

    /**
     * <p>Toggle current state with animation</p>
     */
    public void toggle() {
        if (isVisible()) {
            hide();
        } else {
            show();
        }
    }

    /**
     * <p>Toggle current state without animation</p>
     */
    public void toggleImmediately() {
        if (isVisible()) {
            hideImmediately();
        } else {
            showImmediately();
        }
    }

    /**
     * Hide *
     *
     * @param immediately immediately
     */
    private void hide(boolean immediately) {
        mAnimationProcessor.endAnimation();
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                hideCard(immediately, -mViewHeight);
                break;
            case Gravity.BOTTOM:
                hideCard(immediately, mViewHeight);
                break;
            case Gravity.START:
                if (immediately) {
                    if (mBuilder.mSliderView.getWidth() > 0) {
                        mBuilder.mSliderView.setTranslationX(-mViewWidth);
                        notifyPercentChanged(100);
                    } else {
                        mBuilder.mStartState = HIDDEN;
                    }
                } else {
                    mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationX(), mBuilder.mSliderView.getHeight());
                }
                break;
            case Gravity.END:
                if (immediately) {
                    if (mBuilder.mSliderView.getWidth() > 0) {
                        mBuilder.mSliderView.setTranslationX(mViewWidth);
                        notifyPercentChanged(100);
                    } else {
                        mBuilder.mStartState = HIDDEN;
                    }
                } else {
                    mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationX(), mBuilder.mSliderView.getHeight());
                }
                break;
        }
    }

    private void hideCard(boolean immediately, float value) {
        if (immediately) {
            if (mBuilder.mSliderView.getHeight() > 0) {
                mBuilder.mSliderView.setTranslationY(value);
                notifyPercentChanged(100);
            } else {
                mBuilder.mStartState = HIDDEN;
            }
        } else {
            mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationY(), mBuilder.mSliderView.getHeight());
        }
    }

    /**
     * Show *
     *
     * @param immediately immediately
     */
    private void show(boolean immediately) {
        mAnimationProcessor.endAnimation();
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                showCard(immediately);
                break;
            case Gravity.BOTTOM:
                showCard(immediately);
                break;
            case Gravity.START:
                if (immediately) {
                    if (mBuilder.mSliderView.getWidth() > 0) {
                        mBuilder.mSliderView.setTranslationX(0);
                        notifyPercentChanged(0);
                    } else {
                        mBuilder.mStartState = SHOWED;
                    }
                } else {
                    mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationX(), 0);
                }
                break;
            case Gravity.END:
                if (immediately) {
                    if (mBuilder.mSliderView.getWidth() > 0) {
                        mBuilder.mSliderView.setTranslationX(0);
                        notifyPercentChanged(0);
                    } else {
                        mBuilder.mStartState = SHOWED;
                    }
                } else {
                    mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationX(), 0);
                }
                break;
        }
    }

    private void showCard(boolean immediately) {
        if (immediately) {
            if (mBuilder.mSliderView.getHeight() > 0) {
                mBuilder.mSliderView.setTranslationY(0);
                notifyPercentChanged(0);
            } else {
                mBuilder.mStartState = SHOWED;
            }
        } else {
            float translationY = mBuilder.mSliderView.getTranslationY();
            LogUtil.info(TAG, "translationY: " + translationY);
            mAnimationProcessor.setValuesAndStart(mBuilder.mSliderView.getTranslationY(), 0);
        }
    }

    /**
     * On touch event boolean
     *
     * @param component  component
     * @param touchEvent touch event
     * @return the boolean
     */
    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        if (component == mBuilder.mSliderView) {
            LogUtil.info(TAG, " onTouchEvent mSliderView " + touchEvent.getAction());
        }

        if (component == mBuilder.mAlsoScrollView) {
            LogUtil.info(TAG, " onTouchEvent mAlsoScrollView");
        }
        if (mAnimationProcessor.isAnimationRunning()) {
            return false;
        }

        if (!mBuilder.mGesturesEnabled) {
            mBuilder.mSliderView.simulateClick();
            return true;
        }
        LogUtil.info(TAG, " onTouchEvent mVerticalTouchConsumer " + touchEvent.getAction());
        boolean consumed;
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                consumed = mVerticalTouchConsumer.consumeTopToBottom(component, touchEvent);
                break;
            case Gravity.BOTTOM:
                consumed = mVerticalTouchConsumer.consumeBottomToTop(component, touchEvent);
                break;
            case Gravity.START:
                consumed = mHorizontalTouchConsumer.consumeStartToEnd(component, touchEvent);
                break;
            case Gravity.END:
                consumed = mHorizontalTouchConsumer.consumeEndToStart(component, touchEvent);
                break;
            default:
                throw new IllegalArgumentException("You are using not supported gravity");
        }
        if (!consumed) {
            mBuilder.mSliderView.simulateClick();
        }
        return true;
    }

    /**
     * 重写animatorValue
     *
     * @param animatorValue animator value
     * @param value         value
     */
    @Override
    public void onUpdate(AnimatorValue animatorValue, float value) {
        switch (mBuilder.mStartGravity) {
            case Gravity.TOP:
                onAnimationUpdateTopToBottom(value);
                break;
            case Gravity.BOTTOM:
                onAnimationUpdateBottomToTop(value);
                break;
            case Gravity.START:
                onAnimationUpdateStartToEnd(value);
                break;
            case Gravity.END:
                onAnimationUpdateEndToStart(value);
                break;
        }
    }

    /**
     * On animation update top to bottom *
     *
     * @param value value
     */
    private void onAnimationUpdateTopToBottom(float value) {
        mBuilder.mSliderView.setTranslationY(value);
        float visibleDistance = mBuilder.mSliderView.getTop() - mBuilder.mSliderView.getContentPositionY();
        float percents = (visibleDistance) * 100 / mViewHeight;
        notifyPercentChanged(percents);
    }

    /**
     * On animation update bottom to top *
     *
     * @param value value
     */
    private void onAnimationUpdateBottomToTop(float value) {
        mBuilder.mSliderView.setTranslationY(value);
        float visibleDistance = mBuilder.mSliderView.getContentPositionY() - mBuilder.mSliderView.getTop();
        float percents = (visibleDistance) * 100 / mViewHeight;
        notifyPercentChanged(percents);
    }

    /**
     * On animation update start to end *
     *
     * @param value value
     */
    private void onAnimationUpdateStartToEnd(float value) {
        mBuilder.mSliderView.setTranslationX(value);
        float visibleDistance = mBuilder.mSliderView.getContentPositionX() - getStart();
        float percents = (visibleDistance) * 100 / -mViewWidth;
        notifyPercentChanged(percents);
    }

    /**
     * On animation update end to start *
     *
     * @param value value
     */
    private void onAnimationUpdateEndToStart(float value) {
        mBuilder.mSliderView.setTranslationX(value);
        float visibleDistance = mBuilder.mSliderView.getContentPositionX() - getStart();
        float percents = (visibleDistance) * 100 / mViewWidth;
        notifyPercentChanged(percents);
    }

    /**
     * Get start int
     *
     * @return the int
     */
    private int getStart() {
        if (mBuilder.mIsRTL) {
            return mBuilder.mSliderView.getRight();
        } else {
            return mBuilder.mSliderView.getLeft();
        }
    }

    /**
     * Notify percent changed *
     *
     * @param percent percent
     */
    @Override
    public void notifyPercentChanged(float percent) {
        percent = percent > 100 ? 100 : percent;
        percent = percent < 0 ? 0 : percent;
        if (percent == 100) {
            mBuilder.mSliderView.setVisibility(Component.HIDE);
            notifyVisibilityChanged(Component.HIDE);
        } else {
            mBuilder.mSliderView.setVisibility(Component.VISIBLE);
            if (percent == 0) {
                notifyVisibilityChanged(Component.VISIBLE);
            }
        }
        if (mAnimationProcessor.getSlideAnimationTo() == 0 && mBuilder.mHideKeyboard) {
            hideSoftInput();
        }

        if (!mBuilder.mListeners.isEmpty()) {
            for (int i = 0; i < mBuilder.mListeners.size(); i++) {
                Listener listener = mBuilder.mListeners.get(i);
                if (listener != null) {
                    if (listener instanceof Listener.Slide) {
                        Listener.Slide slide = (Listener.Slide) listener;
                        slide.onSlide(percent);
                        logValue(i, "onSlide", percent);
                    }
                } else {
                    logError(i, "onSlide");
                }
            }
        }
    }

    /**
     * Notify visibility changed *
     *
     * @param visibility visibility
     */
    @Override
    public void notifyVisibilityChanged(int visibility) {
        if (!mBuilder.mListeners.isEmpty()) {
            for (int i = 0; i < mBuilder.mListeners.size(); i++) {
                Listener listener = mBuilder.mListeners.get(i);
                if (listener != null) {
                    if (listener instanceof Listener.Visibility) {
                        Listener.Visibility vis = (Listener.Visibility) listener;
                        vis.onVisibilityChanged(visibility);
                        logValue(i, "onVisibilityChanged", visibility == Component.VISIBLE ? "VISIBLE" : visibility == Component.HIDE ? "GONE" : visibility);
                    }
                } else {
                    logError(i, "onVisibilityChanged");
                }
            }
        }
        switch (visibility) {
            case Component.VISIBLE:
                mCurrentState = SHOWED;
                break;
            case Component.HIDE:
                mCurrentState = HIDDEN;
                break;
        }
    }

    /**
     * On start *
     *
     * @param animator animator
     */
    @Override
    public void onStart(Animator animator) {

    }

    /**
     * On stop *
     *
     * @param animator animator
     */
    @Override
    public void onStop(Animator animator) {

    }

    /**
     * On cancel *
     *
     * @param animator animator
     */
    @Override
    public void onCancel(Animator animator) {

    }

    /**
     * On end *
     *
     * @param animator animator
     */
    @Override
    public void onEnd(Animator animator) {

    }

    /**
     * On pause *
     *
     * @param animator animator
     */
    @Override
    public void onPause(Animator animator) {

    }

    /**
     * On resume *
     *
     * @param animator animator
     */
    @Override
    public void onResume(Animator animator) {

    }

    /**
     * Log value *
     *
     * @param listener listener
     * @param method   method
     * @param message  message
     */
    private void logValue(int listener, String method, Object message) {
        if (mBuilder.mDebug) {
            LogUtil.info(TAG, String.format(Locale.ROOT, "Listener(%1s) (%2$-23s) value = %3$s", listener, method, message));
        }
    }

    /**
     * Log error *
     *
     * @param listener listener
     * @param method   method
     */
    private void logError(int listener, String method) {
        if (mBuilder.mDebug) {
            LogUtil.info(TAG, String.format(Locale.ROOT, "Listener(%1s) (%2$-23s) Listener is null, skip notification...", listener, method));
        }
    }

    /**
     * <p>Available start states</p>
     */
    public enum State {

        /**
         * State hidden is equal {@link }
         */
        HIDDEN,

        /**
         * State showed is equal {@link}
         */
        SHOWED
    }

    /**
     * Start vector@IntDef(value = {Gravity.START, Gravity.END, Gravity.TOP, Gravity.BOTTOM})
     */
    @Retention(RetentionPolicy.SOURCE)
    @interface StartVector {
    }

    /**
     * <p>Interface to listen to all handled events taking place in the slider</p>
     */
    public interface Listener {

        /**
         * Slide
         */
        interface Slide extends Listener {

            /**
             * On slide *
             *
             * @param percent percents of complete slide <b color="#EF6C00">(100 = HIDDEN, 0 = SHOWED)</b>
             */
            void onSlide(float percent);
        }

        /**
         * Visibility
         */
        interface Visibility extends Listener {

            /**
             * On visibility changed *
             *
             * @param visibility (<b>GONE</b> or <b>VISIBLE</b>)
             */
            void onVisibilityChanged(int visibility);
        }

        /**
         * Events
         */
        interface Events extends Visibility, Slide {
        }
    }
}
